package com.group.itms.routes.services;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

import com.group.itms.routes.DatabaseHelper;
import com.group.itms.routes.ThisApplication;
import com.group.itms.routes.data.Advertisement;
import com.group.itms.routes.data.BObject;
import com.group.itms.routes.data.BObjectOption;
import com.group.itms.routes.data.BObjectTopic;
import com.group.itms.routes.data.ObjectGps;
import com.group.itms.routes.data.ObjectMap;
import com.group.itms.routes.data.ObjectType;
import com.group.itms.routes.data.Option;
import com.group.itms.routes.data.Topic;
import com.group.itms.routes.provider.DataProvider;
import com.group.itms.routes.provider.ProviderException;
import com.group.itms.routes.stub.BObjectOptionStub;
import com.group.itms.routes.stub.BObjectStub;
import com.group.itms.routes.stub.BObjectTopicStub;
import com.group.itms.routes.stub.ObjectGpsStub;
import com.group.itms.routes.stub.ObjectMapStub;
import com.group.itms.routes.stub.OptionStub;
import com.j256.ormlite.android.apptools.OpenHelperManager;
import com.j256.ormlite.dao.Dao;

public class SyncService extends Service {

	public static final int SYNC_OK = 0;
	public static final int SYNC_IO_EXCEPTION = 1;
	public static final int SYNC_JSON_EXCEPTION = 2;
	public static final int SYNC_SQLITE_ERROR = 3;

	static final String SYNC_MANAGER_CALLBACK_KEY = "sync_manager_callback";
	
	private static final String TAG = "SyncService";

	private DatabaseHelper mHelper;
	private LocalBinder mLocalBinder = new LocalBinder();

	/** Bridge between SyncService and SyncManager**/
	private SyncCallbacks mSMCallback;
	
	private HandlerThread mSyncThread;

	private SyncListener mSyncListener;
	private Looper mLooper;
	private boolean mSyncStarted;
	
	private NotificationHelper mNotifier;

	// BEGIN INNER CLASSES
	public class LocalBinder extends Binder {

		public SyncService getService() {
			return SyncService.this;
		}
	}

	public static interface SyncListener {
		/**
		 * Called when SyncService completes
		 * 
		 * @param statusCode
		 *            - one of these: <b>SYNC_OK, SYNC_PROVIDER_ERROR,
		 *            SYNC_SQLITE_ERROR</b>
		 * **/
		void onSyncDone(int statusCode);
	}
	// END INNER CLASSES

	public static Intent getIntent() {
		return new Intent(ThisApplication.getInstance(), SyncService.class);
	}

	public void registerSyncListener(SyncListener listener) {
		registerSyncListener(listener, null);
	}

	public void registerSyncListener(SyncListener listener, Looper looper) {
		mSyncListener = listener;
		mLooper = looper;
	}

	public void unregisterSyncListener(SyncListener listener) {
		if (mSyncListener == null || mSyncListener != listener) {
			Log.v(TAG, "Specified listener was not registered, ignoring...");
			return;
		}
		mSyncListener = null;
		Log.v(TAG, "Unregistered listener");
	}

	@Override
	public void onCreate() {
		super.onCreate();
		Log.v(TAG, "created SyncService");
		mHelper = OpenHelperManager.getHelper(this, DatabaseHelper.class);
		mSyncThread = new HandlerThread("SyncServiceThread");
		mSyncThread.start();
		Log.v(TAG, "Started " + mSyncThread);
		mNotifier = new NotificationHelper(this);
		try {
			mSMCallback = (SyncCallbacks) ThisApplication.
					getInstance().getAttribute(SYNC_MANAGER_CALLBACK_KEY);
		} catch (Exception e) { }
		if (mSMCallback != null) {
			Log.d(TAG, "Registered SyncManagerCallback");
		}
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (!mSyncStarted) {
			startSync();
		}
		return super.onStartCommand(intent, flags, startId);
	}

	private void startSync() {
		mSyncStarted = true;
		Log.v(TAG, "Started SyncService");
		// obtain looper of syncThread
		Looper syncLooper = mSyncThread.getLooper();
		Handler syncHandler = new Handler(syncLooper);
		syncHandler.post(new Runnable() {

			@Override
			public void run() {
				sync();
			}
		});
		Log.v(TAG, "Posted sync task to " + syncHandler);
	}

	@Override
	public IBinder onBind(Intent intent) {
		return mLocalBinder;
	}

	private void sync() {
		fireOnSyncStart();
		mNotifier.notifySyncInProgress();
		try {
			doSync();
		} catch (Exception e) {
			fireOnSyncError(e);
		}
		mNotifier.cancelSyncNofitication();
		fireOnSyncStop();
		stopSelf();
	}

	private void fireOnSyncStart() {
		if (mSMCallback != null) {
			mSMCallback.onSyncStart(this);
		}
	}

	private void fireOnSyncError(Exception e) {
		if (mSMCallback != null) {
			mSMCallback.onSyncError(e);
		}
	}

	private void fireOnSyncStop() {
		if (mSMCallback != null) {
			mSMCallback.onSyncStop(this);
		}
	}
	
	private void doSync() {
		DataProvider provider = DataProvider.getInstance();
		StubContainer stubs;
		try {
			stubs = downloadStubs(provider);
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} catch (ProviderException proex) {
			if (proex.getType() == ProviderException.IO_EXCEPTION) {
				dispatchSyncDone(SYNC_IO_EXCEPTION);
			} else {
				dispatchSyncDone(SYNC_JSON_EXCEPTION);
			}
			Log.e(TAG, "Unable to download stubs", proex);
			return;
		}
		StubConverter converter = new StubConverter(stubs);
		long start, end;
		start = System.currentTimeMillis();
		List<BObject> objects = converter.getObjects();
		List<Option> options = converter.getOptions();
		List<BObjectOption> objectOptions = converter.getObjectOptions();
		List<BObjectTopic> objectTopics = converter.getObjectTopics();
		List<ObjectGps> gpsObjects = converter.getGpsObjects();
		List<ObjectMap> objectMaps = converter.getObjectMaps();
		end = System.currentTimeMillis();
		Log.d(TAG, "stub convertation took " + (end - start) + " milliseconds");
		
		try {
			start = System.currentTimeMillis();
			saveBObjects(objects);
			saveOptions(options);
			saveObjectOptions(objectOptions);
			saveObjectTopics(objectTopics);
			saveTopics(stubs.topics);
			saveGpsObjects(gpsObjects);
			saveObjectMaps(objectMaps);
			saveAdvertisements(stubs.advertisements);
			end = System.currentTimeMillis();
			Log.i(TAG, "Database updated succesfully");
			Log.d(TAG, "save to database took " + (end - start) + " milliseconds");
		} catch (SQLException e) {
			Log.e(TAG, "Error while updating database", e);
			dispatchSyncDone(SYNC_SQLITE_ERROR);
			return;
		}
		provider.markCompleted();
		dispatchSyncDone(SYNC_OK);
	}

	private void dispatchSyncDone(int statusCode) {
		if (mLooper == null) {
			// invoke on this thread
			doDispatchSyncDone(statusCode);
		} else {
			// post to looper's queue
			postDispatchOnSyncDone(statusCode);
		}
	}

	private void doDispatchSyncDone(int statusCode) {
		SyncListener l = mSyncListener;
		if (l != null) {
			l.onSyncDone(statusCode);
		}
	}

	private void postDispatchOnSyncDone(final int statusCode) {
		new Handler(mLooper).post(new Runnable() {

			@Override
			public void run() {
				doDispatchSyncDone(statusCode);
			}
		});
	}

	private StubContainer downloadStubs(DataProvider provider) throws ProviderException {
		Log.v(TAG, "Begin downloading stubs");
		long start = System.currentTimeMillis();
		provider.setIgnoreErrors(true);
		StubContainer stubContainer = new StubContainer();
		stubContainer.stubObjects = provider.getObjects();
		stubContainer.stubOptions = provider.getOptions();
		stubContainer.stubObjectOptions = provider.getObjectOptions();
		stubContainer.stubObjectTopics = provider.getObjectTopics();
		stubContainer.topics = provider.getTopics();
		stubContainer.gpsObjects = provider.getGpsObjects();
		stubContainer.stubObjectMaps = provider.getObjectMaps();
		stubContainer.advertisements = provider.getAdvertisements();
		int errors = provider.getErrors();
		if (errors == 0) {
			Log.i(TAG, "Done downloading stubs");
		} else {
			Log.w(TAG, errors + " errors ignored");
		}
		long end = System.currentTimeMillis();
		Log.d(TAG, "download took " + (end - start) + " milliseconds");
		return stubContainer;
	}

	private void saveBObjects(List<BObject> objects) throws SQLException {
		Dao<BObject, Integer> dao = mHelper.getObjectDao();
		Dao<ObjectType, Integer> typeDao = mHelper.getTypeDao();
		for (BObject obj : objects) {
			typeDao.createOrUpdate(obj.getType());
			dao.createOrUpdate(obj);
		}
		Log.v(TAG, "Saved objects");
	}

	private void saveTopics(List<Topic> topics) throws SQLException {
		Dao<Topic, Integer> dao = mHelper.getTopicDao();
		for (Topic topic : topics) {
			dao.createOrUpdate(topic);
		}
		Log.v(TAG, "Saved topics");
	}

	private void saveOptions(List<Option> options) throws SQLException {
		Dao<Option, Integer> dao = mHelper.getOptionDao();
		Dao<ObjectType, Integer> typeDao = mHelper.getTypeDao();
		for (Option opt : options) {
			typeDao.createOrUpdate(opt.getObjectType());
			dao.createOrUpdate(opt);
		}
		Log.v(TAG, "Saved options");
	}

	private void saveObjectOptions(List<BObjectOption> objectOptions)
			throws SQLException {
		Dao<BObjectOption, Integer> dao = mHelper.getObjectOptionDao();
		for (BObjectOption objOpt : objectOptions) {
			dao.createOrUpdate(objOpt);
		}
		Log.v(TAG, "Saved objectOptions");
	}

	private void saveObjectTopics(List<BObjectTopic> objectTopics) throws SQLException {
		Dao<BObjectTopic, Integer> dao = mHelper.getObjectTopicDao();
		for (BObjectTopic objTopic : objectTopics) {
			dao.createOrUpdate(objTopic);
		}
		Log.v(TAG, "Saved objectTopics");
	}

	private void saveGpsObjects(List<ObjectGps> gpsObjects) throws SQLException {
		Dao<ObjectGps, Integer> dao = mHelper.getGpsDao();
		for (ObjectGps og : gpsObjects) {
			dao.createOrUpdate(og);
		}
		Log.v(TAG, "Saved gpsObjects");
	}
	
	private void saveObjectMaps(List<ObjectMap> objectMaps) throws SQLException {
		Dao<ObjectMap, Integer> dao = mHelper.getObjectMapDao();
		for (ObjectMap om : objectMaps) {
			dao.createOrUpdate(om);
		}
		Log.v(TAG, "Saved objectMaps");
	}
	
	private void saveAdvertisements(List<Advertisement> advList) throws SQLException {
		Dao<Advertisement, Integer> dao = mHelper.getAdvertDao();
		for (Advertisement ad : advList) {
			dao.createOrUpdate(ad);
		}
		Log.v(TAG, "Saved advertisements");
	}
	
	@Override
	public void onDestroy() {
		super.onDestroy();
		if (mSyncThread.quit()) {
			Log.v(TAG, "Quited looper");
		}
		try {
			mSyncThread.join();
		} catch (InterruptedException e) {
		}
		Log.v(TAG, "SyncThread finished it's job, releasing helper");
		OpenHelperManager.releaseHelper();
		Log.v(TAG, "destroyed SyncService");
	}

	private static class StubContainer {
		List<BObjectStub> stubObjects;
		List<BObjectOptionStub> stubObjectOptions;
		List<BObjectTopicStub> stubObjectTopics;
		List<OptionStub> stubOptions;
		List<Topic> topics;
		List<ObjectGpsStub> gpsObjects;
		List<ObjectMapStub> stubObjectMaps;
		List<Advertisement> advertisements;
	}

	private class StubConverter {

		StubContainer stubs;

		List<BObject> objectsCache;
		List<Option> optionsCache;
		List<Topic> topicsCache;

		StubConverter(StubContainer stubs) {
			this.stubs = stubs;
			this.topicsCache = stubs.topics;
		}

		List<BObject> getObjects() {
			List<BObjectStub> stubObjects = stubs.stubObjects;
			List<BObject> objects = new ArrayList<BObject>(stubObjects.size());
			for (BObjectStub objStub : stubObjects) {
				BObject obj = new BObject();
				obj.setActive(objStub.isActive());
				obj.setId(objStub.getId());
				obj.setName(objStub.getName());
				obj.setObjInd(objStub.getObjInd());
				obj.setText(objStub.getText());
				obj.setType(ObjectType.getInstance(objStub.getTypeId()));
				obj.setColor(objStub.getColor());
				objects.add(obj);
			}
			objectsCache = objects;
			int size = objects.size();
			for (int i = 0; i < size; i++) {
				BObject obj = objects.get(i);
				BObjectStub stubObj = stubObjects.get(i);
				obj.setParent(findById(objectsCache, stubObj.getParentId()));
			}
			return objects;
		}

		List<Option> getOptions() {
			List<OptionStub> stubOptions = stubs.stubOptions;
			List<Option> options = new ArrayList<Option>(stubOptions.size());
			for (OptionStub optStub : stubOptions) {
				Option opt = new Option();
				opt.setActive(optStub.isActive());
				opt.setId(optStub.getId());
				opt.setName(optStub.getName());
				opt.setObjectType(ObjectType.getInstance(optStub.getObjectTypeId()));
				options.add(opt);
			}
			optionsCache = options;
			return options;
		}

		List<BObjectOption> getObjectOptions() {
			checkObjectsCache();
			checkOptionsCache();
			List<BObjectOptionStub> stubObjectOptions = stubs.stubObjectOptions;
			List<BObjectOption> objectOptions = new ArrayList<BObjectOption>(
					stubObjectOptions.size());
			for (BObjectOptionStub ooStub : stubObjectOptions) {
				BObjectOption oo = new BObjectOption();
				oo.setId(ooStub.getId());
				oo.setObject(findById(objectsCache, ooStub.getObjectId()));
				oo.setOption(findById(optionsCache, ooStub.getOptionId()));
				oo.setActive(ooStub.isActive());
				oo.setText(ooStub.getText());
				objectOptions.add(oo);
			}
			return objectOptions;
		}

		List<BObjectTopic> getObjectTopics() {
			checkObjectsCache();
			List<BObjectTopicStub> stubObjectTopics = stubs.stubObjectTopics;
			List<BObjectTopic> objectTopics = new ArrayList<BObjectTopic>(
					stubObjectTopics.size());
			for (BObjectTopicStub otStub : stubObjectTopics) {
				BObjectTopic ot = new BObjectTopic();
				ot.setId(otStub.getId());
				ot.setObject(findById(objectsCache, otStub.getObjectId()));
				ot.setTopic(findById(topicsCache, otStub.getTopicId()));
				ot.setActive(otStub.isActive());
				objectTopics.add(ot);
			}
			return objectTopics;
		}

		List<ObjectGps> getGpsObjects() {
			checkObjectsCache();
			List<ObjectGpsStub> gpsObjectsStub = stubs.gpsObjects;
			List<ObjectGps> gpsObjects = new ArrayList<ObjectGps>(gpsObjectsStub.size());
			for (ObjectGpsStub ogStub : gpsObjectsStub) {
				ObjectGps og = new ObjectGps();
				og.setId(ogStub.getId());
				og.setObject(findById(objectsCache, ogStub.getObjectId()));
				og.setX(ogStub.getX());
				og.setY(ogStub.getY());
				og.setZ(ogStub.getZ());
				og.setActive(ogStub.isActive());
				gpsObjects.add(og);
			}
			return gpsObjects;
		}
		
		List<ObjectMap> getObjectMaps() {
			checkObjectsCache();
			List<ObjectMapStub> objectMapsStub = stubs.stubObjectMaps;
			List<ObjectMap> objectMaps = new ArrayList<ObjectMap>(objectMapsStub.size());
			for (ObjectMapStub omStub : objectMapsStub) {
				ObjectMap om = new ObjectMap();
				om.setActive(omStub.isActive());
				om.setId(omStub.getId());
				om.setMapUrl(omStub.getMapUrl());
				om.setObject(findById(objectsCache, omStub.getObject()));
				om.setX1(omStub.getX1());
				om.setY1(omStub.getY1());
				om.setX2(omStub.getX2());
				om.setY2(omStub.getY2());
				om.setX3(omStub.getX3());
				om.setY3(omStub.getY3());
				om.setX4(omStub.getX4());
				om.setY4(omStub.getY4());
				objectMaps.add(om);
			}
			
			while (true) {
				int dc = ObjectMapStub.getDownloadersCount();
				if (dc == 0) {
					Log.i(TAG, "done downloading maps");	
					break;
				}
				Log.v(TAG, "waiting for " + dc + " mapdownloaders to finish");
				try {
					Thread.sleep(400);
				} catch (InterruptedException e) {
				}
			}
			int size = objectMaps.size();
			for (int i = 0; i < size; i++) {
				ObjectMapStub oms = objectMapsStub.get(i);
				byte[] map = oms.getMap();
				objectMaps.get(i).setMap(map);
				if (map == null) {
					Log.e(TAG, oms + " has null map data");
				}
			}
			
			return objectMaps;
		}
		
		void checkObjectsCache() {
			if (objectsCache == null) {
				throw new RuntimeException("objectsCache is null, "
						+ "getObjects() has not been called yet");
			}
		}

		void checkOptionsCache() {
			if (optionsCache == null) {
				throw new RuntimeException("optionsCache is null, "
						+ "getOptions() has not been called yet");
			}
		}

		@SuppressWarnings("unchecked")
		<T> T findById(List<T> list, int id) {
			if (id == 0 || list == null || list.size() == 0) {
				// id == 0 means has no parent. yeah :)
				return null;
			}
			Class<?> cls = list.get(0).getClass();
			for (T item : list) {
				try {
					Field idFiled = cls.getDeclaredField("id");
					idFiled.setAccessible(true);
					if (idFiled.get(item).equals(id)) {
						return item;
					}
				} catch (NoSuchFieldException e) {
					// continue
				} catch (IllegalArgumentException e) {
					throw new RuntimeException(e);
				} catch (IllegalAccessException e) {
					throw new RuntimeException(e);
				}
			}
			Log.d(TAG, "could not find object " + cls + " in cache, trying to find in database");
			try {
				return (T) lazyFindById(cls, id);
			} catch (SQLException e) {
				Log.e(TAG, "unable to find object of type: " + cls + " with id: " + id);
				return null;
			}
		}
		
		<T, ID> T lazyFindById(Class<T> clazz, ID id) throws SQLException {
			Dao<T, ID> dao = mHelper.getDao(clazz);
			return dao.queryForId(id);
		}

	}

}
